﻿#region License

/*
 Copyright 2014 - 2015 Nikita Bernthaler
 JsonSerializer.cs is part of SFXLibrary.

 SFXLibrary is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 SFXLibrary is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with SFXLibrary. If not, see <http://www.gnu.org/licenses/>.
*/

#endregion License

namespace SFXLibrary.JSON
{
    #region

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.Data;
    using System.Dynamic;
    using System.Globalization;
    using System.IO;
    using System.Text;

    #endregion

    internal sealed class JSONSerializer
    {
        private readonly Dictionary<object, int> _cirobj = new Dictionary<object, int>();
        private readonly Dictionary<string, int> _globalTypes = new Dictionary<string, int>();
        private readonly int _maxDepth;
        private readonly JSONParameters _params;
        private readonly bool _useEscapedUnicode;
        private StringBuilder _before = new StringBuilder();
        private int _currentDepth;
        private StringBuilder _output = new StringBuilder();
        private bool _typesWritten;

        internal JSONSerializer(JSONParameters param)
        {
            _params = param;
            _useEscapedUnicode = _params.UseEscapedUnicode;
            _maxDepth = _params.SerializerMaxDepth;
        }

        internal string ConvertToJSON(object obj)
        {
            WriteValue(obj);

            string str;
            if (_params.UsingGlobalTypes && _globalTypes != null && _globalTypes.Count > 0)
            {
                var sb = _before;
                sb.Append("\"$types\":{");
                var pendingSeparator = false;
                foreach (var kv in _globalTypes)
                {
                    if (pendingSeparator)
                        sb.Append(',');
                    pendingSeparator = true;
                    sb.Append('\"');
                    sb.Append(kv.Key);
                    sb.Append("\":\"");
                    sb.Append(kv.Value);
                    sb.Append('\"');
                }
                sb.Append("},");
                sb.Append(_output);
                str = sb.ToString();
            }
            else
                str = _output.ToString();

            return str;
        }

        private void WriteValue(object obj)
        {
            if (obj == null || obj is DBNull)
                _output.Append("null");
            else if (obj is string || obj is char)
                WriteString(obj.ToString());
            else if (obj is Guid)
                WriteGuid((Guid) obj);
            else if (obj is bool)
                _output.Append(((bool) obj) ? "true" : "false");
            else if (obj is int || obj is long || obj is double || obj is decimal || obj is float || obj is byte || obj is short || obj is sbyte ||
                     obj is ushort || obj is uint || obj is ulong)
                _output.Append(((IConvertible) obj).ToString(NumberFormatInfo.InvariantInfo));
            else if (obj is DateTime)
                WriteDateTime((DateTime) obj);
            else if (_params.KvStyleStringDictionary == false && obj is IDictionary && obj.GetType().IsGenericType &&
                     obj.GetType().GetGenericArguments()[0] == typeof (string))
                WriteStringDictionary((IDictionary) obj);
            else if (_params.KvStyleStringDictionary == false && obj is ExpandoObject)
                WriteStringDictionary((IDictionary<string, object>) obj);
            else
            {
                var dic = obj as IDictionary;
                if (dic != null)
                    WriteDictionary(dic);
                else
                {
                    var ds = obj as DataSet;
                    if (ds != null)
                        WriteDataset(ds);
                    else
                    {
                        var dt = obj as DataTable;
                        if (dt != null)
                            WriteDataTable(dt);
                        else
                        {
                            var bytes = obj as byte[];
                            if (bytes != null)
                                WriteBytes(bytes);
                            else
                            {
                                var dictionary = obj as StringDictionary;
                                if (dictionary != null)
                                    WriteSd(dictionary);
                                else
                                {
                                    var collection = obj as NameValueCollection;
                                    if (collection != null)
                                        WriteNv(collection);
                                    else
                                    {
                                        var array = obj as IEnumerable;
                                        if (array != null)
                                            WriteArray(array);
                                        else
                                        {
                                            var @enum = obj as Enum;
                                            if (@enum != null)
                                                WriteEnum(@enum);
                                            else if (Reflection.Instance.IsTypeRegistered(obj.GetType()))
                                                WriteCustom(obj);
                                            else
                                                WriteObject(obj);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        private void WriteNv(NameValueCollection nameValueCollection)
        {
            _output.Append('{');

            var pendingSeparator = false;

            foreach (string key in nameValueCollection)
            {
                if (_params.SerializeNullValues == false && (nameValueCollection[key] == null))
                {
                }
                else
                {
                    if (pendingSeparator)
                        _output.Append(',');
                    WritePair(_params.SerializeToLowerCaseNames ? key.ToLower() : key, nameValueCollection[key]);
                    pendingSeparator = true;
                }
            }
            _output.Append('}');
        }

        private void WriteSd(StringDictionary stringDictionary)
        {
            _output.Append('{');

            var pendingSeparator = false;

            foreach (DictionaryEntry entry in stringDictionary)
            {
                if (_params.SerializeNullValues == false && (entry.Value == null))
                {
                }
                else
                {
                    if (pendingSeparator)
                        _output.Append(',');

                    var k = (string) entry.Key;
                    WritePair(_params.SerializeToLowerCaseNames ? k.ToLower() : k, entry.Value);
                    pendingSeparator = true;
                }
            }
            _output.Append('}');
        }

        private void WriteCustom(object obj)
        {
            Serialize s;
            Reflection.Instance.CustomSerializer.TryGetValue(obj.GetType(), out s);
            WriteStringFast(s(obj));
        }

        private void WriteEnum(Enum e)
        {
            if (_params.UseValuesOfEnums)
                WriteValue(Convert.ToInt32(e));
            else
                WriteStringFast(e.ToString());
        }

        private void WriteGuid(Guid g)
        {
            if (_params.UseFastGuid == false)
                WriteStringFast(g.ToString());
            else
                WriteBytes(g.ToByteArray());
        }

        private void WriteBytes(byte[] bytes)
        {
            WriteStringFast(Convert.ToBase64String(bytes, 0, bytes.Length, Base64FormattingOptions.None));
        }

        private void WriteDateTime(DateTime dateTime)
        {
            var dt = dateTime;
            if (_params.UseUTCDateTime)
                dt = dateTime.ToUniversalTime();

            _output.Append('\"');
            _output.Append(dt.Year.ToString("0000", NumberFormatInfo.InvariantInfo));
            _output.Append('-');
            _output.Append(dt.Month.ToString("00", NumberFormatInfo.InvariantInfo));
            _output.Append('-');
            _output.Append(dt.Day.ToString("00", NumberFormatInfo.InvariantInfo));
            _output.Append('T');
            _output.Append(dt.Hour.ToString("00", NumberFormatInfo.InvariantInfo));
            _output.Append(':');
            _output.Append(dt.Minute.ToString("00", NumberFormatInfo.InvariantInfo));
            _output.Append(':');
            _output.Append(dt.Second.ToString("00", NumberFormatInfo.InvariantInfo));
            if (_params.DateTimeMilliseconds)
            {
                _output.Append('.');
                _output.Append(dt.Millisecond.ToString("000", NumberFormatInfo.InvariantInfo));
            }
            if (_params.UseUTCDateTime)
                _output.Append('Z');

            _output.Append('\"');
        }

        private void WriteObject(object obj)
        {
            int i;
            if (_cirobj.TryGetValue(obj, out i) == false)
                _cirobj.Add(obj, _cirobj.Count + 1);
            else
            {
                if (_currentDepth > 0 && _params.InlineCircularReferences == false)
                {
                    _output.Append("{\"$i\":" + i + "}");
                    return;
                }
            }
            _currentDepth++;
            if (_maxDepth > _currentDepth)
            {
                if (_params.UsingGlobalTypes == false)
                    _output.Append('{');
                else
                {
                    if (_typesWritten == false)
                    {
                        _output.Append('{');
                        _before = _output;
                        _output = new StringBuilder();
                    }
                    else
                        _output.Append('{');
                }
                _typesWritten = true;

                var map = new Dictionary<string, string>();
                var t = obj.GetType();
                var append = false;
                if (_params.UseExtensions)
                {
                    if (_params.UsingGlobalTypes == false)
                        WritePairFast("$type", Reflection.Instance.GetTypeAssemblyName(t));
                    else
                    {
                        int dt;
                        var ct = Reflection.Instance.GetTypeAssemblyName(t);
                        if (_globalTypes.TryGetValue(ct, out dt) == false)
                        {
                            dt = _globalTypes.Count + 1;
                            _globalTypes.Add(ct, dt);
                        }
                        WritePairFast("$type", dt.ToString());
                    }
                    append = true;
                }

                var g = Reflection.Instance.GetGetters(t, _params.ShowReadOnlyProperties, _params.IgnoreAttributes);
                var c = g.Length;
                for (var ii = 0; ii < c; ii++)
                {
                    var p = g[ii];
                    try
                    {
                        var o = p.Getter(obj);
                        if (_params.SerializeNullValues || (o != null && !(o is DBNull)))
                        {
                            if (append)
                                _output.Append(',');
                            WritePair(_params.SerializeToLowerCaseNames ? p.LcName : p.Name, o);
                            if (o != null && _params.UseExtensions)
                            {
                                var tt = o.GetType();
                                if (tt == typeof (Object))
                                    map.Add(p.Name, tt.ToString());
                            }
                            append = true;
                        }
                    }
                    catch
                    {
                        if (append)
                            _output.Append(',');
                        WritePairFast((_params.SerializeToLowerCaseNames ? p.LcName : p.Name), "[exception]");
                        append = true;
                    }
                }
                if (map.Count > 0 && _params.UseExtensions)
                {
                    _output.Append(",\"$map\":");
                    WriteStringDictionary(map);
                }
                _output.Append('}');
            }
            else
            {
                _output.Append("\"[max_deep_reached]\"");
            }
            _currentDepth--;
        }

        private void WritePairFast(string name, string value)
        {
            WriteStringFast(name);

            _output.Append(':');

            WriteStringFast(value);
        }

        private void WritePair(string name, object value)
        {
            WriteStringFast(name);

            _output.Append(':');

            WriteValue(value);
        }

        private void WriteArray(IEnumerable array)
        {
            _output.Append('[');

            var pendingSeperator = false;

            foreach (var obj in array)
            {
                if (pendingSeperator)
                    _output.Append(',');

                WriteValue(obj);

                pendingSeperator = true;
            }
            _output.Append(']');
        }

        private void WriteStringDictionary(IDictionary dic)
        {
            _output.Append('{');

            var pendingSeparator = false;

            foreach (DictionaryEntry entry in dic)
            {
                if (_params.SerializeNullValues == false && (entry.Value == null))
                {
                }
                else
                {
                    if (pendingSeparator)
                        _output.Append(',');

                    var k = (string) entry.Key;
                    WritePair(_params.SerializeToLowerCaseNames ? k.ToLower() : k, entry.Value);
                    pendingSeparator = true;
                }
            }
            _output.Append('}');
        }

        private void WriteStringDictionary(IDictionary<string, object> dic)
        {
            _output.Append('{');
            var pendingSeparator = false;
            foreach (var entry in dic)
            {
                if (_params.SerializeNullValues == false && (entry.Value == null))
                {
                }
                else
                {
                    if (pendingSeparator)
                        _output.Append(',');
                    var k = entry.Key;

                    WritePair(_params.SerializeToLowerCaseNames ? k.ToLower() : k, entry.Value);
                    pendingSeparator = true;
                }
            }
            _output.Append('}');
        }

        private void WriteDictionary(IDictionary dic)
        {
            _output.Append('[');

            var pendingSeparator = false;

            foreach (DictionaryEntry entry in dic)
            {
                if (pendingSeparator)
                    _output.Append(',');
                _output.Append('{');
                WritePair("k", entry.Key);
                _output.Append(",");
                WritePair("v", entry.Value);
                _output.Append('}');

                pendingSeparator = true;
            }
            _output.Append(']');
        }

        private void WriteStringFast(string s)
        {
            _output.Append('\"');
            _output.Append(s);
            _output.Append('\"');
        }

        private void WriteString(string s)
        {
            _output.Append('\"');

            var runIndex = -1;
            var l = s.Length;
            for (var index = 0; index < l; ++index)
            {
                var c = s[index];

                if (_useEscapedUnicode)
                {
                    if (c >= ' ' && c < 128 && c != '\"' && c != '\\')
                    {
                        if (runIndex == -1)
                            runIndex = index;

                        continue;
                    }
                }
                else
                {
                    if (c != '\t' && c != '\n' && c != '\r' && c != '\"' && c != '\\')
                    {
                        if (runIndex == -1)
                            runIndex = index;

                        continue;
                    }
                }

                if (runIndex != -1)
                {
                    _output.Append(s, runIndex, index - runIndex);
                    runIndex = -1;
                }

                switch (c)
                {
                    case '\t':
                        _output.Append("\\t");
                        break;

                    case '\r':
                        _output.Append("\\r");
                        break;

                    case '\n':
                        _output.Append("\\n");
                        break;

                    case '"':
                    case '\\':
                        _output.Append('\\');
                        _output.Append(c);
                        break;

                    default:
                        if (_useEscapedUnicode)
                        {
                            _output.Append("\\u");
                            _output.Append(((int) c).ToString("X4", NumberFormatInfo.InvariantInfo));
                        }
                        else
                            _output.Append(c);

                        break;
                }
            }

            if (runIndex != -1)
                _output.Append(s, runIndex, s.Length - runIndex);

            _output.Append('\"');
        }

        private DatasetSchema GetSchema(DataTable ds)
        {
            if (ds == null)
                return null;

            var m = new DatasetSchema {Info = new List<string>(), Name = ds.TableName};

            foreach (DataColumn c in ds.Columns)
            {
                m.Info.Add(ds.TableName);
                m.Info.Add(c.ColumnName);
                m.Info.Add(c.DataType.ToString());
            }
            return m;
        }

        private DatasetSchema GetSchema(DataSet ds)
        {
            if (ds == null)
                return null;

            var m = new DatasetSchema {Info = new List<string>(), Name = ds.DataSetName};

            foreach (DataTable t in ds.Tables)
            {
                foreach (DataColumn c in t.Columns)
                {
                    m.Info.Add(t.TableName);
                    m.Info.Add(c.ColumnName);
                    m.Info.Add(c.DataType.ToString());
                }
            }
            return m;
        }

        private string GetXmlSchema(DataTable dt)
        {
            using (var writer = new StringWriter())
            {
                dt.WriteXmlSchema(writer);
                return dt.ToString();
            }
        }

        private void WriteDataset(DataSet ds)
        {
            _output.Append('{');
            if (_params.UseExtensions)
            {
                WritePair("$schema", _params.UseOptimizedDatasetSchema ? (object) GetSchema(ds) : ds.GetXmlSchema());
                _output.Append(',');
            }
            var tablesep = false;
            foreach (DataTable table in ds.Tables)
            {
                if (tablesep)
                    _output.Append(',');
                tablesep = true;
                WriteDataTableData(table);
            }
            _output.Append('}');
        }

        private void WriteDataTableData(DataTable table)
        {
            _output.Append('\"');
            _output.Append(table.TableName);
            _output.Append("\":[");
            var cols = table.Columns;
            var rowseparator = false;
            foreach (DataRow row in table.Rows)
            {
                if (rowseparator)
                    _output.Append(',');
                rowseparator = true;
                _output.Append('[');

                var pendingSeperator = false;
                foreach (DataColumn column in cols)
                {
                    if (pendingSeperator)
                        _output.Append(',');
                    WriteValue(row[column]);
                    pendingSeperator = true;
                }
                _output.Append(']');
            }

            _output.Append(']');
        }

        private void WriteDataTable(DataTable dt)
        {
            _output.Append('{');
            if (_params.UseExtensions)
            {
                WritePair("$schema", _params.UseOptimizedDatasetSchema ? (object) GetSchema(dt) : GetXmlSchema(dt));
                _output.Append(',');
            }
            WriteDataTableData(dt);
            _output.Append('}');
        }
    }
}